#!/usr/bin/python3.9
# -*- coding: utf-8 -*-
# @Time    : 2021/10/22 20:45
# @Author  : YHSimon

import numpy as np
from scipy.io import loadmat
import matplotlib.pyplot as plt

mat = loadmat('ex8_movies.mat')
print(mat.keys())
Y, R = mat['Y'], mat['R']
nm, nu = Y.shape
nf = 100
print(Y.shape, R.shape)  # (1682, 943) (1682, 943)

Y[0].sum() / R[0].sum()  # 分子代表第一个电影的总分数，分母代表这部电影有多少评分数据
# "Visualize the ratings matrix"
fig = plt.figure(figsize=(8, 8 * (1682. / 943.)))
plt.imshow(Y, cmap='rainbow')
plt.colorbar()
plt.ylabel('Movies (%d)' % nm, fontsize=20)
plt.xlabel('Users (%d)' % nu, fontsize=20)
plt.show()

# 实现代价函数
mat = loadmat('ex8_movieParams.mat')
X = mat['X']
Theta = mat['Theta']
nu = int(mat['num_users'])
nm = int(mat['num_movies'])
nf = int(mat['num_features'])
# For now, reduce the data set size so that this runs faster
nu = 4;
nm = 5;
nf = 3
X = X[:nm, :nf]
Theta = Theta[:nu, :nf]
Y = Y[:nm, :nu]
R = R[:nm, :nu]


def serialize(X, Theta):
    '''展开参数'''
    return np.r_[X.flatten(), Theta.flatten()]


def deserialize(seq, nm, nu, nf):
    '''提取参数'''
    return seq[:nm * nf].reshape(nm, nf), seq[nm * nf:].reshape(nu, nf)


def cofiCostFunc(params, Y, R, nm, nu, nf, l=0):
    """
    params : 拉成一维之后的参数向量(X, Theta)
    Y : 评分矩阵 (nm, nu)
    R ：0-1矩阵，表示用户对某一电影有无评分
    nu : 用户数量
    nm : 电影数量
    nf : 自定义的特征的维度
    l : lambda for regularization
    """
    X, Theta = deserialize(params, nm, nu, nf)

    # (X@Theta)*R含义如下： 因为X@Theta是我们用自定义参数算的评分，但是有些电影本来是没有人
    # 评分的，存储在R中，0-1表示。将这两个相乘，得到的值就是我们要的已经被评分过的电影的预测分数。
    error = 0.5 * np.square((X @ Theta.T - Y) * R).sum()
    reg1 = .5 * l * np.square(Theta).sum()
    reg2 = .5 * l * np.square(X).sum()

    return error + reg1 + reg2


cofiCostFunc(serialize(X, Theta), Y, R, nm, nu, nf), cofiCostFunc(serialize(X, Theta), Y, R, nm, nu, nf, 1.5)


# (22.224603725685675, 31.34405624427422)


def cofiGradient(params, Y, R, nm, nu, nf, l=0):
    """
    计算X和Theta的梯度，并序列化输出。
    """
    X, Theta = deserialize(params, nm, nu, nf)

    X_grad = ((X @ Theta.T - Y) * R) @ Theta + l * X
    Theta_grad = ((X @ Theta.T - Y) * R).T @ X + l * Theta

    return serialize(X_grad, Theta_grad)


def checkGradient(params, Y, myR, nm, nu, nf, l=0.):
    """
    Let's check my gradient computation real quick
    """
    print('Numerical Gradient \t cofiGrad \t\t Difference')

    # 分析出来的梯度
    grad = cofiGradient(params, Y, myR, nm, nu, nf, l)

    # 用 微小的e 来计算数值梯度。
    e = 0.0001
    nparams = len(params)
    e_vec = np.zeros(nparams)

    # Choose 10 random elements of param vector and compute the numerical gradient
    # 每次只能改变e_vec中的一个值，并在计算完数值梯度后要还原。
    for i in range(10):
        idx = np.random.randint(0, nparams)
        e_vec[idx] = e
        loss1 = cofiCostFunc(params - e_vec, Y, myR, nm, nu, nf, l)
        loss2 = cofiCostFunc(params + e_vec, Y, myR, nm, nu, nf, l)
        numgrad = (loss2 - loss1) / (2 * e)
        e_vec[idx] = 0
        diff = np.linalg.norm(numgrad - grad[idx]) / np.linalg.norm(numgrad + grad[idx])
        print('%0.15f \t %0.15f \t %0.15f' % (numgrad, grad[idx], diff))


print("Checking gradient with lambda = 0...")
checkGradient(serialize(X, Theta), Y, R, nm, nu, nf)
print("\nChecking gradient with lambda = 1.5...")
checkGradient(serialize(X, Theta), Y, R, nm, nu, nf, l=1.5)

movies = []  # 包含所有电影的列表
with open('movie_ids.txt', 'r', encoding='utf-8') as f:
    for line in f:
        # movies.append(' '.join(line.strip().split(' ')[1:]))
        movies.append(' '.join(line.strip().split(' ')[1:]))

my_ratings = np.zeros((1682, 1))

my_ratings[0] = 4
my_ratings[97] = 2
my_ratings[6] = 3
my_ratings[11] = 5
my_ratings[53] = 4
my_ratings[63] = 5
my_ratings[65] = 3
my_ratings[68] = 5
my_ratings[182] = 4
my_ratings[225] = 5
my_ratings[354] = 5

for i in range(len(my_ratings)):
    if my_ratings[i] > 0:
        print(my_ratings[i], movies[i])

mat = loadmat('ex8_movies.mat')
Y, R = mat['Y'], mat['R']
Y = np.c_[Y, my_ratings]  # (1682, 944)
R = np.c_[R, my_ratings != 0]  # (1682, 944)
nm, nu = Y.shape
nf = 10  # 我们使用10维的特征向量


def normalizeRatings(Y, R):
    """
    The mean is only counting movies that were rated
    """
    Ymean = (Y.sum(axis=1) / R.sum(axis=1)).reshape(-1, 1)
    #     Ynorm = (Y - Ymean)*R  # 这里也要注意不要归一化未评分的数据
    Ynorm = (Y - Ymean) * R  # 这里也要注意不要归一化未评分的数据
    return Ynorm, Ymean


Ynorm, Ymean = normalizeRatings(Y, R)
X = np.random.random((nm, nf))
Theta = np.random.random((nu, nf))
params = serialize(X, Theta)
l = 10

import scipy.optimize as opt

res = opt.minimize(fun=cofiCostFunc,
                   x0=params,
                   args=(Y, R, nm, nu, nf, l),
                   method='TNC',
                   jac=cofiGradient,
                   options={'maxiter': 100})

ret = res.x
fit_X, fit_Theta = deserialize(ret, nm, nu, nf)
# 所有用户的剧场分数矩阵
pred_mat = fit_X @ fit_Theta.T
# 最后一个用户的预测分数， 也就是我们刚才添加的用户
pred = pred_mat[:,-1] + Ymean.flatten()
pred_sorted_idx = np.argsort(pred)[::-1] # 排序并翻转，使之从大到小排列
print("Top recommendations for you:")
for i in range(10):
    print('Predicting rating %0.1f for movie %s.' \
          % (pred[pred_sorted_idx[i]], movies[pred_sorted_idx[i]]))

print("\nOriginal ratings provided:")
for i in range(len(my_ratings)):
    if my_ratings[i] > 0:
        print('Rated %d for movie %s.' % (my_ratings[i], movies[i]))

